﻿using System;
using System.Net;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;

using WebServer.Modules;

namespace WebServer
{
	public sealed class UID
	{
		private string _Base;
		/// <summary>
		/// Gets the base string that was initially used to create this UID.
		/// </summary>
		public string Base { get { return _Base; } }

		private string _Value;
		/// <summary>
		/// Gets the value of the UID.
		/// </summary>
		public string Value { get { return _Value; } }

		/// <summary>
		/// Creates a new instance of UID (Unique Identification)
		/// </summary>
		/// <param name="fromBase">The base string to use to generate the UID value.</param>
		public UID(string fromBase)
		{
			_Base = fromBase;
			_Value = Cryptography.MD5Hash(_Base).Replace("-", "");
		}
	}

	/// <summary>
	/// Provides the means to display web-pages, both static and virtual, upon request
	/// </summary>
	public abstract class WebObject : IWebObject
	{
		private UID _UID;
		/// <summary>
		/// Gets the UID for this WebObject.
		/// </summary>
		public UID UID
		{
			get
			{
				if (_UID == null)
					_UID = new UID(GetType().FullName);

				return _UID;
			}
		}

		private bool _RealTime = false;
		/// <summary>
		/// Gets or Sets a value indecating whether this instance of WebObject should output data in 'real time'. 
		/// This will cause all Elements added to this WebObject to be sent directly to the request output stream.
		/// </summary>
		public bool RealTime { get { return _RealTime; } set { _RealTime = value; } }

		private bool _Deleted = false;
		/// <summary>
		/// Gets a value indecating whether this instance of WebObject has been deleted.
		/// </summary>
		public bool Deleted { get { return _Deleted; } }

		/// <summary>
		/// Gets the VirtualName of this WebObject.
		/// </summary>
		public abstract string VirtualName { get; }

		private string _Name = String.Empty;
		/// <summary>
		/// Gets or Sets the Name of this WebObject.
		/// </summary>
		public virtual string Name { get { return _Name; } set { _Name = value; } }

		private bool _PhpScript = false;
		/// <summary>
		/// Gets or Sets a value indecating whether this WebObject is a PHP script.
		/// </summary>
		public virtual bool PhpScript { get { return _PhpScript; } set { _PhpScript = value; } }

		private bool _AspScript = false;
		/// <summary>
		/// Gets or Sets a value indecating whether this WebObject is an ASP.NET script.
		/// </summary>
		public virtual bool AspScript { get { return _AspScript; } set { _AspScript = value; } }

		private string _ContentType = String.Empty;
		/// <summary>
		/// Gets or Sets the ContentType associated with this WebObject.
		/// </summary>
		public virtual string ContentType { get { return _ContentType; } set { _ContentType = value; } }

		private Client _Client;
		/// <summary>
		/// Gets or Sets the Client Connection requesting this WebObject.
		/// </summary>
		public virtual Client Client { get { return _Client; } set { _Client = value; } }

		private HttpListenerContext _Context;
		/// <summary>
		/// Gets the initial HttpListenerContext that holds the request and response data related to this instance of WebObject.
		/// </summary>
		public HttpListenerContext Context { get { return _Context; } }

		private Uri _RequestURL;
		/// <summary>
		/// Gets the URL requested that retrieved this WebObject.
		/// </summary>
		public Uri RequestURL { get { return _RequestURL; } }

		private List<IWebObjectElement> _Elements = new List<IWebObjectElement>();
		/// <summary>
		/// Gets the list of elements contained within this WebObject, that may be compiled upon request
		/// </summary>
		public List<IWebObjectElement> Elements { get { return _Elements; } }

		private Dictionary<string, string> _Queries = new Dictionary<string, string>();
		/// <summary>
		/// Gets the list of formatted 'key=value' pairs parsed directly from the RequestURL.Query string
		/// </summary>
		public Dictionary<string, string> Queries { get { return _Queries; } }

		private string _Body = String.Empty;
		/// <summary>
		/// Gets or Sets the contents of this WebObject, after compilation
		/// </summary>
		public virtual string Body { get { return _Body; } set { _Body = value; } }

		private byte[] _RawData = new byte[0];
		/// <summary>
		/// Gets the raw byte data value of this WebObject. 
		/// The WebObject must be Compiled before this variable is populated.
		/// </summary>
		public byte[] RawData { get { return _RawData; } }

		private FileInfo _FileData;
		/// <summary>
		/// Gets or Sets the physical FileInfo data associated with this WebObject.
		/// </summary>
		public virtual FileInfo FileData { get { return _FileData; } set { _FileData = value; } }

		/// <summary>
		/// Constructs a new instance of WebObject with the given Client connection and URL information
		/// </summary>
		/// <param name="client">Client Connection requesting this WebObject</param>
		/// <param name="url">URL that was used to retrieve this WebObject</param>
		public WebObject(HttpListenerContext ctx, Client client, Uri url)
		{
			_Context = ctx;
			_Client = client;
			_RequestURL = url;

			ParseQueryString();
		}

		/// <summary>
		/// Constructs a new instance of WebObject with the given Client connection and URL information
		/// </summary>
		/// <param name="client">Client Connection requesting this WebObject</param>
		/// <param name="url">URL that was used to retrieve this WebObject</param>
		/// <param name="elements">A list of pre-defined element instances to construct this WebObject with</param>
		public WebObject(HttpListenerContext ctx, Client client, Uri url, IWebObjectElement[] elements)
			: this(ctx, client, url)
		{
			InsertElement(elements);
		}

		/// <summary>
		/// Creates an instance of WebObject with the pre-defined UID. 
		/// This constructor provides a way for the internal file-system to handle virtual WebObjects and should not be used to construct a WebObject instance.
		/// </summary>
		/// <param name="uid">Pre-defined UID.</param>
		public WebObject(UID uid)
		{ _UID = uid; }

		private void ParseQueryString()
		{
			if (String.IsNullOrEmpty(_RequestURL.Query.Replace("?", String.Empty)))
				return;

			string[] split = _RequestURL.Query.Replace("?", String.Empty).Split(new char[] { '&', ';', '|' }, StringSplitOptions.RemoveEmptyEntries);

			for (int i = 0; i < split.Length; i++)
			{
				if (String.IsNullOrEmpty(split[i]))
					continue;

				string[] kvp = split[i].Split('=');

				if (kvp.Length == 2)
				{
					string key = kvp[0], val = kvp[1];

					if (!_Queries.ContainsKey(key))
						_Queries.Add(key, val);
					else
						_Queries[key] = val;
				}
			}
		}

		/// <summary>
		/// Gets the value of the query that matches the key
		/// </summary>
		/// <param name="key">KEy of the query to lookup</param>
		/// <returns>The value associated with the query key</returns>
		public string GetQuery(string key)
		{
			string val = String.Empty;

			if (_Queries.ContainsKey(key))
				val = _Queries[key];

			return val;
		}

		/// <summary>
		/// Inserts a list of pre-defined elements into the elements list
		/// </summary>
		/// <param name="elements">List of pre-defined elements to insert</param>
		public virtual void InsertElement(IWebObjectElement[] elements)
		{
			foreach (IWebObjectElement element in elements)
			{
				InsertElement(element);
			}
		}

		/// <summary>
		/// Inserts an element into the element list. 
		/// If RealTime is true, the element will be compiled and sent to the request output stream
		/// and will not be added to the Elements collection.
		/// </summary>
		/// <param name="element">Element to insert</param>
		public virtual void InsertElement(IWebObjectElement element)
		{
			if (element != null)
			{
				if (_RealTime)
				{
					string data = String.Empty;

					if (element.OnBeforeCompile())
					{
						data = element.Compile();
						element.OnAfterCompile();

						if (data != element.Content)
							data = element.Content;

						byte[] buffer = Encoding.Default.GetBytes(data);

						StreamData(_Context.Response.OutputStream, buffer);
					}
				}
				else
				{
					_Elements.Add(element);
				}
			}
		}

		/// <summary>
		/// Adds an 'html' element to this WebObject
		/// </summary>
		/// <param name="html">HTML code</param>
		public virtual void AddHTML(string html)
		{ InsertElement(new HTMLElement(this, html)); }

		/// <summary>
		/// Adds an 'html' element to this WebObject with formatted code
		/// </summary>
		/// <param name="format">String format</param>
		/// <param name="args">Format arguments</param>
		public virtual void AddHTML(string format, params object[] args)
		{ InsertElement(new HTMLElement(this, String.Format(format, args))); }

		/// <summary>
		/// Adds a 'text' element to this WebObject
		/// </summary>
		/// <param name="text">Plain text</param>
		public virtual void AddText(string text)
		{ InsertElement(new TextElement(this, text)); }

		/// <summary>
		/// Adds a 'text' element to this WebObject with formatting
		/// </summary>
		/// <param name="format">String format</param>
		/// <param name="args">Format arguments</param>
		public virtual void AddText(string format, params object[] args)
		{ InsertElement(new TextElement(this, String.Format(format, args))); }

		/// <summary>
		/// Adds a 'byte' element to this WebObject.
		/// </summary>
		/// <param name="b">Raw byte data to add.</param>
		public virtual void AddByte(byte b)
		{ InsertElement(new ByteElement(this, b)); }

		/// <summary>
		/// Adds a 'byte array' element to this WebObject.
		/// </summary>
		/// <param name="bytes">Raw byte data array to add.</param>
		public virtual void AddByte(byte[] bytes)
		{
			InsertElement(new ByteElement(this, bytes));
		}

		/// <summary>
		/// Outputs the contents of FileData to the given output stream. 
		/// If FileData is null, the RawData buffer will be sent to the output stream.
		/// </summary>
		/// <param name="output">The stream for output data.</param>
		public virtual void StreamData(Stream output)
		{
			StreamData(output, FileBuffer.DefaultBufferLength);
		}

		/// <summary>
		/// Outputs the contents of FileData to the given output stream. 
		/// If FileData is null, the RawData buffer will be sent to the output stream.
		/// </summary>
		/// <param name="output">The stream for output data.</param>
		/// <param name="maxBufferLength">The maximum buffer length before a FileBuffer is used to output data.</param>
		public virtual void StreamData(Stream output, long maxBufferLength)
		{
			try
			{
				if (output == null)
					return;

				if (_FileData == null)
				{
					if (OnBeforeCompile())
					{
						Compile();
						OnAfterCompile();
					}

					StreamData(output, _RawData);
				}
				else
				{
					_Name = _FileData.Name;

					if (_FileData.Length < maxBufferLength)
					{
						_Body = File.ReadAllText(_FileData.FullName, Encoding.Default);
						_RawData = Encoding.Default.GetBytes(_Body);

						StreamData(output, _RawData);
					}
					else //Too large, use a FileBuffer to chunk it...
					{
						using (FileBuffer buffer = new FileBuffer(_FileData))
						{ buffer.Start(output, maxBufferLength); }
					}
				}
			}
			catch (Exception ex) { Core.ConsoleWriteError(true, ex.ToString()); }
		}

		/// <summary>
		/// Outputs the contents of the given buffer to the output stream.
		/// </summary>
		/// <param name="output">The stream for output data.</param>
		/// <param name="buffer">The buffer to write to the output stream.</param>
		public virtual void StreamData(Stream output, byte[] buffer)
		{
			try
			{
				if (output == null)
					return;

				output.Write(buffer, 0, buffer.Length);

			}
			catch (Exception ex) { Core.ConsoleWriteError(true, ex.ToString()); }
		}

		/// <summary>
		/// Imports the raw data found at the given Url string.
		/// </summary>
		/// <param name="url">Target Url string.</param>
		/// <returns>Raw byte data from the target url.</returns>
		public virtual byte[] Import(string url)
		{
			return Import(url, true);
		}

		/// <summary>
		/// Imports the raw data found at the given Url string.
		/// </summary>
		/// <param name="url">Target Url string.</param>
		/// <param name="add">If true, the imported data is automatically inserted into this WebObject.</param>
		/// <returns>Raw byte data from the target url.</returns>
		public virtual byte[] Import(string url, bool add)
		{
			return Import(new Uri(url), add);
		}

		/// <summary>
		/// Imports the raw data found at the given Url.
		/// </summary>
		/// <param name="url">Target Url.</param>
		/// <returns>Raw byte data from the target url.</returns>
		public virtual byte[] Import(Uri url)
		{
			return Import(url, true);
		}

		/// <summary>
		/// Imports the raw data found at the given Url.
		/// </summary>
		/// <param name="url">Target Url.</param>
		/// <param name="add">If true, the imported data is automatically inserted into this WebObject.</param>
		/// <returns>Raw byte data from the target url.</returns>
		public virtual byte[] Import(Uri url, bool add)
		{
			byte[] rawData = new byte[0];

			using (WebClient client = new WebClient())
			{
				try
				{
					client.Headers["User-Agent"] = "Mozilla/4.0 (Compatible; Windows NT 5.1; MSIE 6.0) (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)";
					client.Encoding = Encoding.Default;
					rawData = client.DownloadData(url);

					if (add)
						AddByte(rawData);
				}
				catch (Exception ex)
				{
					Core.ConsoleWriteLine(ex.ToString());
				}
			}

			return rawData;
		}

		/// <summary>
		/// Compiles all elements associated with this WebObject into a single Body string before being sent back as a request reply.
		/// </summary>
		/// <returns>Body of this WebObject, compiled into a single string.</returns>
		public virtual string Compile()
		{
			_Body = String.Empty;

			for (int i = 0; i < _Elements.Count; i++)
			{
				if (_Elements[i].OnBeforeCompile())
				{
					string data = _Elements[i].Compile();
					_Elements[i].OnAfterCompile();

					if (data != _Elements[i].Content)
						data = _Elements[i].Content;

					_Body += data;
				}
			}

			OnCompile();

			_RawData = Encoding.Default.GetBytes(_Body);

			return _Body;
		}

		/// <summary>
		/// Effectively nulliefies all variables, and clears all arrays associated with this WebObject.
		/// </summary>
		public void Delete()
		{
			_Queries.Clear();
			_Elements.Clear();
			_Deleted = true;
		}

		/// <summary>
		/// Fired when this WebObject is compiled
		/// </summary>
		public virtual void OnCompile() { }
		/// <summary>
		/// Fired just before this WebObject is compiled and determines whether the WebObject should be Compiled
		/// </summary>
		/// <returns>Returns 'true' by default, can be overridden in a derived class to determine whether this WebObject should be compiled or not</returns>
		public virtual bool OnBeforeCompile() { return true; }
		/// <summary>
		/// Fired just after this WebObject is successfully compiled
		/// </summary>
		public virtual void OnAfterCompile() { }
		/// <summary>
		/// Fired when this WebObject is sent as a request reply
		/// </summary>
		public virtual void OnSend() { }
	}
}

namespace WebServer.HTDOCS
{
	/// <summary>
	/// The internal blank webObj used by the WebServer system
	/// </summary>
	public sealed class BlankPage : WebObject
	{
		/// <summary>
		/// Gets or Sets the VirtualName of this WebObject. 
		/// This variable is used for mapping http requests to the correct virtual WebObject.
		/// </summary>
		public override string VirtualName { get { return "blank.vor"; } }

		/// <summary>
		/// Constructs a new 'blank' WebObject
		/// </summary>
		/// <param name="ctx">Listener context that invoked this WebObject.</param>
		/// <param name="client">Client connection that invoked this WebObject.</param>
		/// <param name="url">Requested URL.</param>
		public BlankPage(HttpListenerContext ctx, Client client, Uri url)
			: base(ctx, client, url)
		{
			ContentType = "text/html";
			Name = "Blank Page";
		}

		public BlankPage(UID uid)
			: base(uid)
		{ }
	}

	/// <summary>
	/// The internal error webObj used by the WebServer system
	/// </summary>
	public sealed class ErrorPage : WebObject
	{
		/// <summary>
		/// Gets or Sets the VirtualName of this WebObject. 
		/// This variable is used for mapping http requests to the correct virtual WebObject.
		/// </summary>
		public override string VirtualName { get { return "error.vor"; } }

		private string _Error = "WebObject could not be found.";
		/// <summary>
		/// Stores the Error for this WebObject, set internally by Http Request EventArgs
		/// </summary>
		public string Error { get { return _Error; } set { _Error = value; } }

		/// <summary>
		/// Constructs a new 'error' WebObject
		/// </summary>
		/// <param name="ctx">Listener context that invoked this WebObject.</param>
		/// <param name="client">Client connection that invoked this WebObject.</param>
		/// <param name="url">Requested URL.</param>
		public ErrorPage(HttpListenerContext ctx, Client client, Uri url)
			: base(ctx, client, url)
		{
			ContentType = "text/html";
			Name = "Error Page";

			this.AddHTML("<H1>{0}</H1>", _Error);
		}

		public ErrorPage(UID uid)
			: base(uid)
		{ }
	}

	/// <summary>
	/// The internal Command WebObject used by the WebServer system
	/// </summary>
	public sealed class CommandPage : WebObject
	{
		/// <summary>
		/// Gets or Sets the VirtualName of this WebObject. 
		/// This variable is used for mapping http requests to the correct virtual WebObject.
		/// </summary>
		public override string VirtualName { get { return "cmd.vor"; } }

		/// <summary>
		/// Constructs a new 'command' WebObject
		/// </summary>
		/// <param name="ctx">Listener context that invoked this WebObject.</param>
		/// <param name="client">Client connection that invoked this WebObject.</param>
		/// <param name="url">Requested URL.</param>
		public CommandPage(HttpListenerContext ctx, Client client, Uri url)
			: base(ctx, client, url)
		{
			Name = "Command API";
		}

		public CommandPage(UID uid)
			: base(uid)
		{ }
	}
}
